Skip to content

feat(http): refactor node:http client instrumentation for portability#20393

Open
isaacs wants to merge 3 commits intodevelopfrom
isaacschlueter/portable-http-integration-client
Open

feat(http): refactor node:http client instrumentation for portability#20393
isaacs wants to merge 3 commits intodevelopfrom
isaacschlueter/portable-http-integration-client

Conversation

@isaacs
Copy link
Copy Markdown
Member

@isaacs isaacs commented Apr 20, 2026

Refactor the node:http outgoing request instrumentation so that it can
be applied to non-Node.js environments by patching the http module.

Also, refactor so that the diagnostics_channel and monkeypatching paths
can share code, and so that light and normal node-core instrumentations
can share more of the functionality as well.

To facilitate this, some portable minimal types are vendored in from the
node:http module.

  • If you've added code that should be tested, please add tests.
  • Ensure your code lints and the test suite passes (yarn lint) & (yarn test).
  • Link an issue if there is one related to your pull request. If no issue is linked, one will be auto-generated and linked.

Notes on test changes:

  • The test changes are mostly owing to the fact that more versions are covered by some of the conditions, so the test gets un-indented, because it was previously conditionalTest on a node version.
  • The origin span data changes from the vague (and now incorrect) auto.http.otel.http to auto.http.client for client spans. This also affects a lot of the tests.

Otherwise, all prior behavior should be unchanged, which is reflected in the integration and unit tests.

@isaacs isaacs force-pushed the isaacschlueter/portable-http-integration-client branch from 1f9cef2 to 5ab332a Compare April 20, 2026 01:47
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 20, 2026

size-limit report 📦

Path Size % Change Change
@sentry/browser 26.16 kB - -
@sentry/browser - with treeshaking flags 24.63 kB - -
@sentry/browser (incl. Tracing) 44.13 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 46.34 kB - -
@sentry/browser (incl. Tracing, Profiling) 49.08 kB - -
@sentry/browser (incl. Tracing, Replay) 83.48 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 72.96 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 88.15 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 100.8 kB - -
@sentry/browser (incl. Feedback) 43.4 kB - -
@sentry/browser (incl. sendFeedback) 30.96 kB - -
@sentry/browser (incl. FeedbackAsync) 36.14 kB - -
@sentry/browser (incl. Metrics) 27.44 kB - -
@sentry/browser (incl. Logs) 27.59 kB - -
@sentry/browser (incl. Metrics & Logs) 28.28 kB - -
@sentry/react 27.9 kB - -
@sentry/react (incl. Tracing) 46.36 kB - -
@sentry/vue 31.03 kB - -
@sentry/vue (incl. Tracing) 45.96 kB - -
@sentry/svelte 26.18 kB - -
CDN Bundle 28.84 kB -0.01% -1 B 🔽
CDN Bundle (incl. Tracing) 46.91 kB - -
CDN Bundle (incl. Logs, Metrics) 30.27 kB +0.01% +2 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) 48.03 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 69.35 kB -0.01% -1 B 🔽
CDN Bundle (incl. Tracing, Replay) 84.07 kB -0.01% -2 B 🔽
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 85.14 kB -0.01% -3 B 🔽
CDN Bundle (incl. Tracing, Replay, Feedback) 89.86 kB -0.01% -1 B 🔽
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 90.95 kB -0.02% -13 B 🔽
CDN Bundle - uncompressed 84.55 kB - -
CDN Bundle (incl. Tracing) - uncompressed 140.16 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 88.75 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 143.62 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 212.71 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 257.96 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 261.41 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 271.66 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 275.1 kB - -
@sentry/nextjs (client) 48.85 kB - -
@sentry/sveltekit (client) 44.58 kB - -
@sentry/node-core 59.66 kB +1.04% +610 B 🔺
@sentry/node 162.89 kB -4.38% -7.45 kB 🔽
@sentry/node - without tracing 71.82 kB -25.89% -25.09 kB 🔽
@sentry/aws-serverless 106.5 kB -6.37% -7.25 kB 🔽
@sentry/cloudflare (withSentry) - minified 164.97 kB +0.01% +4 B 🔺
@sentry/cloudflare (withSentry) 417.12 kB +0.01% +21 B 🔺

View base workflow run

@isaacs isaacs force-pushed the isaacschlueter/portable-http-integration-client branch 3 times, most recently from 18b93f6 to fd04ed5 Compare April 20, 2026 16:31
isaacs added a commit that referenced this pull request Apr 20, 2026
Refactor the `node:http` outgoing request instrumentation so that it can
be applied to non-Node.js environments by patching the http module.

Also, refactor so that the diagnostics_channel and monkeypatching paths
can share code, and so that light and normal node-core instrumentations
can share more of the functionality as well.

To facilitate this, some portable minimal types are vendored in from the
`node:http` module.
@isaacs isaacs force-pushed the isaacschlueter/portable-http-integration-client branch from 0877c6e to 1963717 Compare April 20, 2026 19:00
@isaacs isaacs marked this pull request as ready for review April 20, 2026 19:00
@isaacs isaacs requested review from andreiborza and mydea April 20, 2026 19:00
Comment thread packages/core/src/integrations/http/client-subscriptions.ts Outdated
isaacs added a commit that referenced this pull request Apr 20, 2026
Refactor the `node:http` outgoing request instrumentation so that it can
be applied to non-Node.js environments by patching the http module.

Also, refactor so that the diagnostics_channel and monkeypatching paths
can share code, and so that light and normal node-core instrumentations
can share more of the functionality as well.

To facilitate this, some portable minimal types are vendored in from the
`node:http` module.
@isaacs isaacs force-pushed the isaacschlueter/portable-http-integration-client branch from 1963717 to 3e7001c Compare April 20, 2026 19:06
Comment thread packages/core/src/integrations/http/client-subscriptions.ts Outdated
Comment thread packages/core/src/integrations/http/instrument-outgoing-request.ts Outdated
isaacs added a commit that referenced this pull request Apr 20, 2026
Refactor the `node:http` outgoing request instrumentation so that it can
be applied to non-Node.js environments by patching the http module.

Also, refactor so that the diagnostics_channel and monkeypatching paths
can share code, and so that light and normal node-core instrumentations
can share more of the functionality as well.

To facilitate this, some portable minimal types are vendored in from the
`node:http` module.
@isaacs isaacs force-pushed the isaacschlueter/portable-http-integration-client branch from 0307e3a to 640f8f0 Compare April 20, 2026 20:12
Comment thread packages/core/src/integrations/http/client-subscriptions.ts Outdated
Comment thread packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts Outdated
Comment thread packages/core/src/integrations/http/client-subscriptions.ts
Comment thread packages/node-core/src/light/integrations/httpIntegration.ts
Comment thread packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts Outdated
isaacs added a commit that referenced this pull request Apr 20, 2026
Refactor the `node:http` outgoing request instrumentation so that it can
be applied to non-Node.js environments by patching the http module.

Also, refactor so that the diagnostics_channel and monkeypatching paths
can share code, and so that light and normal node-core instrumentations
can share more of the functionality as well.

To facilitate this, some portable minimal types are vendored in from the
`node:http` module.
@isaacs isaacs force-pushed the isaacschlueter/portable-http-integration-client branch from 1b8377c to 1a8d51e Compare April 20, 2026 21:52
Comment thread packages/core/src/integrations/http/client-subscriptions.ts
Comment thread packages/node-core/src/utils/outgoingHttpRequest.ts
Comment thread packages/core/src/integrations/http/client-subscriptions.ts
isaacs added a commit that referenced this pull request Apr 28, 2026
Refactor the `node:http` outgoing request instrumentation so that it can
be applied to non-Node.js environments by patching the http module.

Also, refactor so that the diagnostics_channel and monkeypatching paths
can share code, and so that light and normal node-core instrumentations
can share more of the functionality as well.

To facilitate this, some portable minimal types are vendored in from the
`node:http` module.
@isaacs isaacs force-pushed the isaacschlueter/portable-http-integration-client branch from 645dc0e to e88e5f5 Compare April 28, 2026 16:10
isaacs added a commit that referenced this pull request Apr 28, 2026
Refactor the `node:http` outgoing request instrumentation so that it can
be applied to non-Node.js environments by patching the http module.

Also, refactor so that the diagnostics_channel and monkeypatching paths
can share code, and so that light and normal node-core instrumentations
can share more of the functionality as well.

To facilitate this, some portable minimal types are vendored in from the
`node:http` module.
@isaacs isaacs force-pushed the isaacschlueter/portable-http-integration-client branch from e88e5f5 to ddd2987 Compare April 28, 2026 16:13
@isaacs isaacs marked this pull request as draft April 28, 2026 16:15
Comment thread packages/core/src/integrations/http/inject-trace-propagation-headers.ts Outdated
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ddd2987. Configure here.

Comment thread packages/core/src/integrations/http/client-subscriptions.ts
isaacs added a commit that referenced this pull request Apr 28, 2026
Refactor the `node:http` outgoing request instrumentation so that it can
be applied to non-Node.js environments by patching the http module.

Also, refactor so that the diagnostics_channel and monkeypatching paths
can share code, and so that light and normal node-core instrumentations
can share more of the functionality as well.

To facilitate this, some portable minimal types are vendored in from the
`node:http` module.
@isaacs isaacs force-pushed the isaacschlueter/portable-http-integration-client branch from 24f03e6 to eac6329 Compare April 28, 2026 18:33
@isaacs isaacs marked this pull request as ready for review April 28, 2026 19:44
isaacs added a commit that referenced this pull request Apr 28, 2026
Refactor the `node:http` outgoing request instrumentation so that it can
be applied to non-Node.js environments by patching the http module.

Also, refactor so that the diagnostics_channel and monkeypatching paths
can share code, and so that light and normal node-core instrumentations
can share more of the functionality as well.

To facilitate this, some portable minimal types are vendored in from the
`node:http` module.
@isaacs isaacs force-pushed the isaacschlueter/portable-http-integration-client branch from aacfec1 to edb38de Compare April 28, 2026 20:04
Comment thread packages/node-core/src/light/integrations/httpIntegration.ts
isaacs added a commit that referenced this pull request Apr 28, 2026
Refactor the `node:http` outgoing request instrumentation so that it can
be applied to non-Node.js environments by patching the http module.

Also, refactor so that the diagnostics_channel and monkeypatching paths
can share code, and so that light and normal node-core instrumentations
can share more of the functionality as well.

To facilitate this, some portable minimal types are vendored in from the
`node:http` module.
@isaacs isaacs force-pushed the isaacschlueter/portable-http-integration-client branch from edb38de to 0c11611 Compare April 28, 2026 20:34
isaacs added a commit that referenced this pull request Apr 28, 2026
Refactor the `node:http` outgoing request instrumentation so that it can
be applied to non-Node.js environments by patching the http module.

Also, refactor so that the diagnostics_channel and monkeypatching paths
can share code, and so that light and normal node-core instrumentations
can share more of the functionality as well.

To facilitate this, some portable minimal types are vendored in from the
`node:http` module.
@isaacs isaacs force-pushed the isaacschlueter/portable-http-integration-client branch from 0c11611 to 76df64d Compare April 28, 2026 21:24
@isaacs isaacs requested a review from JPeer264 April 28, 2026 22:34
Copy link
Copy Markdown
Member

@JPeer264 JPeer264 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing changes. LGTM.

@@ -0,0 +1,86 @@
import { createTestServer } from '@sentry-internal/test-utils';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extremely nice that there are tests for this scenario. Maybe it makes sense to display a warning in our docs too: https://docs.sentry.io/platforms/javascript/guides/node/configuration/integrations/http/ (not sure if this is the best place to put it)

const baggageEntry = `${encodeURIComponent(objectKey)}=${encodeURIComponent(objectValue)}`;
const newBaggageHeader = currentIndex === 0 ? baggageEntry : `${baggageHeader},${baggageEntry}`;
if (newBaggageHeader.length > MAX_BAGGAGE_STRING_LENGTH) {
/* v8 ignore start */
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: Why exactly is this needed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a test verifying that it hit the limit, but I didn't feel like it was essential to mock out the debugging and verify the log, so the ignore just marks it as "not tested, but it's fine". This gets the function to full coverage, while marking that we're cheating a bit to do so. Arguably this is a bit lazy, but the v8 ignore comment sort of acts as a TODO.

isaacs added 3 commits April 29, 2026 09:42
This was implemented for the portable Express integration, but others
will need the same functionality, so make it a reusable util.
Refactor the `node:http` outgoing request instrumentation so that it can
be applied to non-Node.js environments by patching the http module.

Also, refactor so that the diagnostics_channel and monkeypatching paths
can share code, and so that light and normal node-core instrumentations
can share more of the functionality as well.

To facilitate this, some portable minimal types are vendored in from the
`node:http` module.
Comment on lines +38 to +69
if (sentryTrace && !request.getHeader('sentry-trace')) {
try {
request.setHeader('sentry-trace', sentryTrace);
DEBUG_BUILD && debug.log(LOG_PREFIX, 'Added sentry-trace header');
} catch (e) {
DEBUG_BUILD &&
debug.error(LOG_PREFIX, 'Failed to set sentry-trace header:', isError(e) ? e.message : 'Unknown error');
}
}

if (traceparent && !request.getHeader('traceparent')) {
try {
request.setHeader('traceparent', traceparent);
DEBUG_BUILD && debug.log(LOG_PREFIX, 'Added traceparent header');
} catch (e) {
DEBUG_BUILD &&
debug.error(LOG_PREFIX, 'Failed to set traceparent header:', isError(e) ? e.message : 'Unknown error');
}
}

if (baggage) {
const merged = mergeBaggageHeaders(request.getHeader('baggage'), baggage);
if (merged) {
try {
request.setHeader('baggage', merged);
DEBUG_BUILD && debug.log(LOG_PREFIX, 'Added baggage header');
} catch (e) {
DEBUG_BUILD &&
debug.error(LOG_PREFIX, 'Failed to set baggage header:', isError(e) ? e.message : 'Unknown error');
}
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: On develop we currently skip setting traceparent and modifying baggage if the request already has a sentry-trace. In this refactoring we only skip overwriting sentry-trace.

See:

const hasExistingSentryTraceHeader = !!request.getHeader('sentry-trace');
if (hasExistingSentryTraceHeader) {
return;
}
if (sentryTrace) {
try {
request.setHeader('sentry-trace', sentryTrace);
DEBUG_BUILD && debug.log(LOG_PREFIX, 'Added sentry-trace header to outgoing request');
} catch (error) {
DEBUG_BUILD &&
debug.error(
LOG_PREFIX,
'Failed to add sentry-trace header to outgoing request:',
isError(error) ? error.message : 'Unknown error',
);
}
}
if (traceparent && !request.getHeader('traceparent')) {
try {
request.setHeader('traceparent', traceparent);
DEBUG_BUILD && debug.log(LOG_PREFIX, 'Added traceparent header to outgoing request');
} catch (error) {
DEBUG_BUILD &&
debug.error(
LOG_PREFIX,
'Failed to add traceparent header to outgoing request:',
isError(error) ? error.message : 'Unknown error',
);
}
}
if (baggage) {
const existingBaggage = request.getHeader('baggage');
const newBaggage = mergeBaggageHeaders(existingBaggage, baggage);
if (newBaggage) {
try {
request.setHeader('baggage', newBaggage);
DEBUG_BUILD && debug.log(LOG_PREFIX, 'Added baggage header to outgoing request');
} catch (error) {
DEBUG_BUILD &&
debug.error(
LOG_PREFIX,
'Failed to add baggage header to outgoing request:',
isError(error) ? error.message : 'Unknown error',
);
}
}
}
}

And in fetch:

// We do not want to set any headers if we already have an existing sentry-trace header.
// sentry-trace is still the source of truth, otherwise we risk mixing up baggage and sentry-trace values.
if (!hasExistingSentryTraceHeader) {
if (sentryTrace) {
requestHeaders.push(SENTRY_TRACE_HEADER, sentryTrace);
}
if (traceparent && _findExistingHeaderIndex(requestHeaders, 'traceparent') === -1) {
requestHeaders.push('traceparent', traceparent);
}
// For baggage, we make sure to merge this into a possibly existing header
const existingBaggageIndex = _findExistingHeaderIndex(requestHeaders, SENTRY_BAGGAGE_HEADER);
if (baggage && existingBaggageIndex === -1) {
requestHeaders.push(SENTRY_BAGGAGE_HEADER, baggage);
} else if (baggage) {
// headers in format [key_0, value_0, key_1, value_1, ...], hence the +1 here
const existingBaggageValue = requestHeaders[existingBaggageIndex + 1];
const merged = mergeBaggageHeaders(existingBaggageValue, baggage);
if (merged) {
requestHeaders[existingBaggageIndex + 1] = merged;
}
}
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, yeah, I think one of the clankers complained about this. In this file, I opted to just keep it as it was already in the code that I was porting, but it's probably a good time to update and make it consistent. Easy enough to update, good catch!

Comment thread packages/core/src/integrations/http/client-patch.ts
Comment on lines +85 to +87
if (getCurrentScope().getScopeData().sdkProcessingMetadata[SUPPRESS_TRACING_KEY] === true) {
return;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: The behavior here changed slightly, on develop we first check for suppression and only then check the ignoreOutgoingRequests hook. So users could now potentially observe these despite being marked for suppressed.

Nothing critical here, just a small deviation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, that's a good point, it is also slightly less efficient to run a userland function than to just check a flag.

? ''
: `:${requestOptions.port}`;
const path = requestOptions.path ? requestOptions.path : '/';
return `${protocol}//${hostname}${port}${path}`;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: Since breadcrumbs now also use this helper, I found out that there's a slight deviation from develop specifically for breadcrumbs when requestOptions.path is already an absolute path. We'd end up with something like http://proxy.localhttp://example.com/foo if the request looked like this:

const req = http.request({
    host: 'proxy.local',
    port: 3128,
    method: 'GET',
    path: 'http://target.example/foo',
  });

Whereas on develop, we would get just http://target.example/foo.

Now, the interesting part is that spans, ignore callbacks, trace propagation etc were all going through this same logic on develop already, so if this is an issue, it was already an issue before.

Breadcrumbs seems to be the only path that did this correctly, maybe we can align on that instead?

const url = new URL(request.path, `${request.protocol}//${host}`);

}
const { outgoingRequestApplyCustomAttributes: applyCustomAttributesOnSpan, ...options } = this.getConfig();
const patchOptions: HttpInstrumentationOptions = {
propagateTrace: options.propagateTraceInOutgoingRequests,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q/l: Do options contain propagateTrace? Wonder if the spreading further down might overwrite it. If not, disregard.

expect(request.setHeader).not.toHaveBeenCalledWith('traceparent', expect.anything());
});

it('merges baggage with existing baggage header', () => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: I think we need another test here to check this does not happen when sentry-trace is already present (regression from what's on develop, see other comment).

Comment thread packages/core/src/utils/url.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants